package us.kg.kotoriforest.schedule.dao;

import us.kg.kotoriforest.schedule.util.JDBCUtil;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * ClassName: us.kg.kotoriforest.schedule.dao.BaseDAO </br>
 * Description: 将 DAO 中的一些重复代码封装起来 </br>
 * DAO 实现类想要使用该类中的方法必须继承该类 </br>
 * <strong>完成了 {@code JDBC} 中的获取 Statement、执行SQL语句、并返回结果的部分</strong>
 *
 * @author Kotori rotroutine@163.com
 * @since 2024/12/2
 */
public abstract class BaseDAO {
    /**
     * 封装 DAO 实现类增、删、改中的重复代码。可以通过该方法实现对表基础的增、删、改操作 </br>
     * 如果当前连接没有开启事务就会自动归还连接；如果开启了事务，就需要调用者根据事务的执行结果自行处理，最后通过{@code JDBCUtil}自行归还连接
     *
     * @param sql          增、删、改的 SQL 语句
     * @param placeholders 可变参数。SQL 语句中占位符的确切数据，如果没有可以不用写
     * @return 影响表中记录的行数。如果 SQL 语句执行正常，返回值应大于 0; 执行错误则返回 0
     * @throws SQLException SQL 语句出现错误则抛出异常
     */
    protected int executeUpdate(String sql, Object... placeholders) throws SQLException {
        // 加载驱动 获取连接
        Connection connection = JDBCUtil.getConnection();

        // 获得预处理的 SQL 语句
        try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
            // 替换 SQL 语句中的占位符
            if (null != placeholders && placeholders.length > 0) {
                for (int i = 0; i < placeholders.length; i++) {
                    preparedStatement.setObject(i + 1, placeholders[i]);
                }
            }

            // 执行 SQL 语句并返回结果
            return preparedStatement.executeUpdate();
        } finally {
            // 回收连接 只有当连接没有开启事务时才直接回收 否则就不会回收 让调用者自行处理归还
            if (connection.getAutoCommit()) {
                // connection.getAutoCommit() 为 true 时代表没有开启事务
                JDBCUtil.closeConnection();
            }
        }
    }

    /**
     * 封装了 ORM 实现类中查询的重复代码。可以实现单行单列、单行多列、多行单列、多行多列的查询</br>
     * 如果当前连接没有开启事务就会自动归还连接；如果开启了事务，就需要调用者根据事务的执行结果自行处理，最后通过{@code JDBCUtil}自行归还连接
     *
     * @param clazz        单行查询结果对应的 ORM 类的类对象
     * @param sql          需要执行的查询 SQL 语句。在写 SQL 语句时，一定要通过 AS 取别名的方式，将列名的_命名法转换成小驼峰命名法，否则会抛出异常
     * @param placeholders 可变参数。SQL 语句中占位符的确切数据，如果没有可以不用写
     * @param <T>          单行查询结果对应的 ORM 类
     * @return 查询结果封装成的 ORM 对象集合，不会返回 null 值
     * @throws SQLException     SQL 语句出现错误，或没有使用小驼峰命名法重命名列名，抛出此异常
     * @throws RuntimeException 方法内部反射出错，请检查 ORM 的类对象有没有传错
     */
    protected <T> List<T> executeQuery(Class<T> clazz, String sql, Object... placeholders) throws SQLException {
        // 记载驱动 获取连接
        Connection connection = JDBCUtil.getConnection();

        try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
            // 替换占位符
            if (null != placeholders && placeholders.length > 0) {
                for (int i = 0; i < placeholders.length; ++i) {
                    preparedStatement.setObject(i + 1, placeholders[i]);
                }
            }

            // 执行 SQL 语句 得到结果集
            try (ResultSet resultSet = preparedStatement.executeQuery()) {
                // 根据结果集中的元数据 得到结果集中的列的个数和列名
                ResultSetMetaData metaData = resultSet.getMetaData();
                int columnCount = metaData.getColumnCount();

                // 使用反射获取 ORM 类的默认构造器 便于之后将查到的数据封装进 ORM 对象中
                Constructor<T> constructor = clazz.getDeclaredConstructor();
                constructor.setAccessible(true);

                // 构建结果集合
                List<T> results = new ArrayList<>();

                // 获取并封装查询结果
                while (resultSet.next()) {
                    T t = constructor.newInstance();

                    for (int i = 1; i <= columnCount; ++i) {
                        // 获取列名
                        String columnName = metaData.getColumnLabel(i);
                        Object value = resultSet.getObject(i);

                        // 通过反射为 ORM 对象中的属性赋值
                        Field field = clazz.getDeclaredField(columnName);
                        field.setAccessible(true);
                        field.set(t, value);
                    }

                    results.add(t);
                }

                return results;

            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } finally {
            if (connection.getAutoCommit()) {
                JDBCUtil.closeConnection();
            }
        }
    }

    /**
     * 功能与{@link BaseDAO#executeQuery(Class, String, Object...)}相同，当查询结果只有一行或者有多行但只想取第一行时可以使用该方法 </br>
     * 如果当前连接没有开启事务就会自动归还连接；如果开启了事务，就需要调用者根据事务的执行结果自行处理，最后通过{@code JDBCUtil}自行归还连接
     *
     * @param clazz        单行查询结果对应的 ORM 类的类对象
     * @param sql          需要执行的查询 SQL 语句。在写 SQL 语句时，一定要通过 AS 取别名的方式，将列名的_命名法转换成小驼峰命名法，否则会抛出异常
     * @param placeholders 可变参数。SQL 语句中占位符的确切数据，如果没有可以不用写
     * @param <T>          单行查询结果对应的 ORM 类
     * @return 查询结果中的第一个值，如果查询没有结果则会返回 null
     * @throws SQLException     SQL 语句出现错误，或没有使用小驼峰命名法重命名列名，抛出此异常
     * @throws RuntimeException 方法内部反射出错，请检查 ORM 的类对象有没有传错
     */
    protected <T> T executeQueryGetFirstElement(Class<T> clazz, String sql, Object... placeholders) throws SQLException {
        List<T> results = executeQuery(clazz, sql, placeholders);
        if (null == results || results.isEmpty()) {
            return null;
        }

        return results.getFirst();
    }

    /**
     * 执行结果是基本数据类型（不包括 char 和 boolean 类型）的查询语句
     *
     * @param clazz        基本数据类型的包装类的的类对象
     * @param sql          需要执行的 SQL 语句
     * @param placeholders 可变参数。SQL 语句中占位符的确切数据，如果没有可以不用写
     * @param <T>          除了 char 和 boolean 外的基本数据类型的包装类
     * @return 查询的结果，如果没有查询到则返回 null
     * @throws SQLException SQL 语句出现错误时抛出此异常
     */
    protected <T extends Number> T executeQueryGetBasicElement(Class<T> clazz, String sql, Object... placeholders) throws SQLException {
        Connection connection = JDBCUtil.getConnection();

        try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
            if (null != placeholders && placeholders.length > 0) {
                for (int i = 0; i < placeholders.length; ++i) {
                    preparedStatement.setObject(i + 1, placeholders[i]);
                }
            }

            ResultSet resultSet = preparedStatement.executeQuery();

            return resultSet.next() ? resultSet.getObject(1, clazz) : null;

        } finally {
            if (connection.getAutoCommit()) {
                JDBCUtil.closeConnection();
            }
        }
    }
}
